package groep1.graph.graphstrategy.line;

import groep1.graph.GraphObject;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

/**
 * A class representing a line as a collection of points.
 * 
 * @author Niels Billen
 * @version 0.1
 * @since 0.1
 */
public abstract class Line {
	/**
	 * Creates a new line from the given points.
	 * 
	 * @param points
	 *            The points of this line.
	 * @throws NullPointerException
	 *             When the given points are null.
	 */
	public Line(Point... points) throws NullPointerException {
		if (points == null)
			throw new NullPointerException("The given points are null!");
		for (Point point : points)
			addPoint(point);
	}

	/**
	 * Creates a new line from the given line.
	 * 
	 * @param line
	 *            The line to create this line from.
	 * @throws NullPointerException
	 *             When the given line is null.
	 */
	public Line(Line line) throws NullPointerException {
		if (line == null)
			throw new NullPointerException("The given line is null!");
		for (Point point : line.getPoints())
			addPoint(point);
	}

	/**
	 * Add's a point to this line.
	 * 
	 * @param point
	 *            The point to add to this line.
	 * @throws NullPointerException
	 *             When the given point is null.
	 */
	public void addPoint(Point point) throws NullPointerException {
		if (point == null)
			throw new NullPointerException("The given point is null!");
		points.add(new Point(point));
	}

	/**
	 * Returns the point at the given index.
	 * 
	 * @param index
	 *            The point at the given index.
	 * @return the point at the given index.
	 */
	public Point getPointAt(int index) {
		if (index < 0 || index >= points.size())
			throw new IndexOutOfBoundsException(
					"The given index is out of bounds!");
		return new Point(points.get(index));
	}

	/**
	 * Returns the last point of the line.
	 * 
	 * @return the last point of the line.
	 */
	public Point getLastPoint() {
		return getPointAt(getNbOfPoints() - 1);
	}

	/**
	 * Returns the number of points in the line.
	 * 
	 * @return the number of points in the line.
	 */
	public int getNbOfPoints() {
		return points.size();
	}

	/**
	 * Returns all the points in the line.
	 * 
	 * @return all the points in the line.
	 */
	public List<Point> getPoints() {
		return new ArrayList<Point>(points);
	}

	/**
	 * The list with all the points.
	 */
	private ArrayList<Point> points = new ArrayList<Point>();

	/**
	 * Returns whether this line overlaps with the given line.
	 * 
	 * @param line
	 *            The line to check the overlap with.
	 * @return true when the lines overlap.
	 */
	public abstract boolean overlap(Line line);

	/**
	 * Returns the length of this line.
	 * 
	 * @return the length of this line.
	 */
	public double getLength() {
		if (points.size() < 2)
			return 0;
		Point p1;
		Point p2;
		double result = 0;

		for (int i = 1; i < points.size(); i++) {
			p1 = getPointAt(i - 1);
			p2 = getPointAt(i);

			result += getDistanceBetween(p1, p2);
		}
		return result;
	}

	/**
	 * Returns the distance between the two given points.
	 * 
	 * @param p1
	 *            The first point.
	 * @param p2
	 *            The second point.
	 * @return the distance between the given points.
	 */
	public static double getDistanceBetween(Point p1, Point p2) {
		if (p1 == null || p2 == null)
			return 0;
		double xDiff;
		double yDiff;

		xDiff = p1.x - p2.x;
		yDiff = p1.y - p2.y;

		return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
	}

	/**
	 * Returns whether the given point lays on the line.
	 * 
	 * @param point
	 *            The point to check.
	 * @return whether the point lays on the line.
	 */
	public boolean overlap(Point point) {
		if (points.size() == 0)
			return false;
		if (points.size() == 0 && points.get(0).equals(point))
			return false;
		Point p1;
		Point p2;
		int minX;
		int minY;
		int maxX;
		int maxY;

		for (int i = 1; i < points.size(); i++) {
			p1 = points.get(i - 1);
			p2 = points.get(i);

			minX = Math.min(p1.x, p2.x);
			maxX = Math.max(p1.x, p2.x);
			minY = Math.max(p1.y, p2.y);
			maxY = Math.max(p1.y, p2.y);

			if (point.x < minX || point.x > maxX)
				continue;
			if (point.y < minY || point.y > maxY)
				continue;

			double left = (p2.getX() - p1.getX()) * point.getY();
			double right = (p2.getY() - p1.getY()) * point.getX() + p2.getX()
					* p1.getY() - p1.getX() * p2.getY();

			if (Math.abs(left - right) < 0.0001)
				return true;
		}

		return false;
	}

	/**
	 * Returns whether the given point lays on the line.
	 * 
	 * @param point
	 *            The point to check.
	 * @return whether the point lays on the line.
	 */
	public boolean overlap(GraphObject obj, int offset) {
		if (points.size() < 2)
			return false;
		Point p1;
		Point p2;

		for (int i = 1; i < points.size(); i++) {
			p1 = points.get(i - 1);
			p2 = points.get(i);

			Line2D.Double l = new Line2D.Double(p1, p2);
			Rectangle2D.Double rect = new Rectangle2D.Double(obj.getPosition()
					.getX() - offset, obj.getPosition().getY() - offset,
					(float) obj.getWidth() + 2 * offset,
					(float) obj.getHeight() + 2 * offset);

			if (l.intersects(rect))
				return true;
		}

		return false;
	}

	/**
	 * Draws this line on the given graphics.
	 * 
	 * @param g
	 *            The graphics to draw upon.
	 */
	public void draw(Graphics g, int x, int y, Color color, boolean thick) {
		g.setColor(color);

		Point p1;
		Point p2;

		if (points.size() == 0)
			return;
		else if (points.size() == 1) {
			p1 = points.get(0);
			g.drawLine(p1.x, p1.y, p1.x, p1.y);
		} else
			for (int i = 1; i < points.size(); i++) {
				p1 = points.get(i - 1);
				p2 = points.get(i);

				g.drawLine(p1.x + x, p1.y + y, p2.x + x, p2.y + y);

				if (thick) {
					g.drawLine(p1.x + x, p1.y + y - 1, p2.x + x, p2.y + y - 1);
					g.drawLine(p1.x + x, p1.y + y + 1, p2.x + x, p2.y + y + 1);

					g.drawLine(p1.x + x + 1, p1.y + y, p2.x + x + 1, p2.y + y);
					g.drawLine(p1.x + x - 1, p1.y + y, p2.x + x - 1, p2.y + y);
				}
			}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("[Line] with points: ");

		for (Point p : points)
			builder.append(p.toString() + " ");

		return builder.toString();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object object) {
		if (object == null)
			return false;
		if (!(object instanceof Line))
			return false;
		Line line = (Line) object;

		if (line.getNbOfPoints() != getNbOfPoints())
			return false;

		for (int i = 0; i < getNbOfPoints(); i++)
			if (!getPointAt(i).equals(line.getPointAt(i)))
				return false;
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	public int hashCode() {
		int result = 0;

		for (Point point : points)
			result += point.hashCode();
		return result;
	}
}
